# iTorrent.py
#
# Copyright (c) 2006 Michael Hobbs
#
# This file is part of iTorrent.
#
# iTorrent is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# iTorrent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with iTorrent; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import os
import socket
import sys
import time
import errno

import BitTorrent_patches
from BitTorrent.defaultargs import get_defaults
from BitTorrent.prefs import Preferences
from BitTorrent import configfile
from BitTorrent import platform

from version import VERSION
import utils
from BTControl import BTControl
from Display import Display
from FeedStore import FeedStore
from RawHTTPServer import RawHTTPServer, RawHTTPRequestHandler
from TorrentFetcher import TorrentFetcher
from FeedFetcher import FeedFetcher
import RawHTTPClient
from RawHTTPClient import CONNECT, DATA, CLOSED, FAILURE
import Resolver
import MacLauncher


RSS_UPDATE = 0
FETCH_ENCLOSURE = 1
FETCH_TORRENT = 2


class HTTPServer(RawHTTPServer):
	def __init__(self, rawServer, display, btControl, config, useHelper):
		self.config = config
		port = config['control_port']
		if useHelper:
			port = 0
		address = ('localhost', port)
		try:
			RawHTTPServer.__init__(self, rawServer, address, RequestHandler)
		except socket.error, e:
			if e[0] not in [errno.EADDRINUSE, errno.EADDRNOTAVAIL, errno.EACCES]:
				raise
			display.error('%s: Port %d cannot be used.\n' \
					'(Is another copy of iTorrent already running?)\n' \
					'Use --control_port option to select a different port.' % \
					(e[1], port))
			# Prompt for feedback before exiting, in case iTorrent was started by double-clicking on iTorrent.py
			raw_input('Press [Enter] to exit')
			sys.exit(0)
		self.display = display
		self.btControl = btControl
		self.feedStore = FeedStore(config['data_dir'], rawServer)
		self.torrentFetcher = TorrentFetcher(btControl, self.feedStore)
		self.feedFetcher = FeedFetcher(rawServer, self.feedStore, self.torrentFetcher, display, config)


class RequestHandler(RawHTTPRequestHandler):
	def __init__(self, *args, **kwargs):
		RawHTTPRequestHandler.__init__(self, *args, **kwargs)
		self.type = None
		self.id = None
		self.stop = False

	# TODO: factor out common code
	def do_GET(self):
		self.socket.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
		if self.path.startswith(utils.TORRENT_PATH):
			self.type = FETCH_TORRENT
			hash = os.path.dirname(self.path[len(utils.TORRENT_PATH):])
			if hash not in self.server.feedStore:
				message = 'Unknown torrent requested. You may need to update your podcast[s].'
				self.server.display.error(message)
				self.send_error(404, message)
				self.done = True
				return
			# TODO 2: see if iTunes looks at content-length for dropped transmissions.
			# If not, see if chunked transfer-encoding works better.
			record = self.server.feedStore[hash]
			self.id = record.url
			self.server.display.info('Fetching torrent: ' + record.url)
			self.send_response(200)
			self.end_headers()
			self.server.torrentFetcher.fetch(record.url, self.wfile)
			return
		if self.path.startswith(utils.ENCLOSURE_PATH):
			self.type = FETCH_ENCLOSURE
			hash = os.path.dirname(self.path[len(utils.ENCLOSURE_PATH):])
			if hash not in self.server.feedStore:
				message = 'Unknown file requested. You may need to update your podcast[s].'
				self.server.display.error(message)
				self.send_error(404, message)
				self.done = True
				return
			# TODO 2: see if iTunes looks at content-length for dropped transmissions.
			# If not, see if chunked transfer-encoding works better.
			# It certainly looks at content-length for longer transmissions. It will
			# drop the connection after content-length bytes have been sent.
			record = self.server.feedStore[hash]
			self.id = record.url
			self.server.display.info('Fetching enclosure: ' + record.url)
			self.send_response(200)
			if record.length:
				self.send_header('Content-Length', str(record.length))
			self.end_headers()
			self.sendEnclosure(record)
			return
		self.type = RSS_UPDATE
		url = self.path[1:]
		self.id = url
		self.server.display.info('Updating RSS feed: ' + url)
		# Immediately spit out about 2K worth of space to prevent iTunes from
		# spiking the CPU while it waits for data.
		self.send_response(200)
		self.send_header('Content-Type', 'text/xml')
		self.end_headers()
		self.wfile.write('<?xml version="1.0" encoding="utf-8"?>\n')
		self.wfile.write(' ' * 2048)
		self.server.feedFetcher.fetch(url, self.wfile, self.headers['host'])

	def connection_lost(self, unused_s):
		self.server.display.info('Connection lost. Cancelling.')
		if self.type == RSS_UPDATE:
			# TODO: test this, plus with prefetch encl, plus with prefetch torrent,
			# plus with prefetch first piece.
			self.server.feedFetcher.stop(self.id)
		elif self.type == FETCH_ENCLOSURE:
			# TODO: just close the socket, don't bother setting a flag.
			self.stop = True
		elif self.type == FETCH_TORRENT:
			self.server.torrentFetcher.stop(self.id)
		# end if

	def sendEnclosure(self, record):
		# A partial file may not exist if we encountered an error while trying to
		# fetch it.
		if os.path.exists(record.partialPath):
			partialFile = file(record.partialPath, 'rb')
			self.wfile.write(partialFile.read())
			skip = [partialFile.tell()]
			partialFile.close()
		else:
			skip = [0]
		url = record.url
		# TODO: forward any Range requests in headers
		RawHTTPClient.get(url, self.server.config.get('http_proxy'), self.server.rawServer, self.callback, record, skip)

	def callback(self, event, data, record, skip):
		if self.stop:
			return False
		if event == CONNECT:
			return True
		if event == DATA:
			if skip[0]:
				skipped = min(len(data), skip[0])
				data = data[skipped:]
				skip[0] -= skipped
			if data:
				self.wfile.write(data)
			return True
		if event == CLOSED:
			record.numFailures = 0
			record.sent = time.time()
			if os.path.exists(record.partialPath):
				utils.remove(record.partialPath)
			self.wfile.close()
			return False
		if event == FAILURE:
			record.numFailures += 1
			message = 'Download of enclosure file %s failed: %s' % (record.url, data)
			self.server.display.error(message)
			# Write bogus text to wfile. Hopefully, it will cause iTunes to abort.
			self.wfile.write('error: ' + message)
			self.wfile.close()
			return False
		# end if

	def version_string(self):
		return 'iTorrent/%s (HTTPServer)' % VERSION

	__pychecker__ = 'no-override'
	def log_request(self, *args):
		# Don't log the requests
		pass

	def log_error(self, *args):
		# Don't log the errors here
		pass


def main(useHelper=False):
	defaults = get_defaults('bittorrent-console')
	config, args = configfile.parse_configuration_and_args(defaults, 'iTorrent',
		sys.argv[1:], 0, 0)
	config = Preferences().initWithDict(config)
	config['tracker_proxy'] = config['http_proxy']
	display = Display()
	btControl = BTControl(config, display)
	os.chdir(config['data_dir'])
	prefix = 'http://localhost'
	if config['control_port'] != 80:
		prefix += ':%d' % config['control_port']
	prefix += '/'

	# TODO: iTunes drops the connection to a torrent download if a concurrent update takes too long. Figure out what the problem is. (Might have been hangups due to DNS queries)
	# TODO: make sure we don't buffer up too much data from socket connections and chew up megabytes of memory.
	# TODO: Allow configuration and feeds.yaml to be re-loaded during runtime.
	Resolver.startResolverThread(btControl.rawServer)
	server = HTTPServer(btControl.rawServer, display, btControl, config, useHelper)
	# TODO: FeedStore should be passed to HTTPServer, not created by it
	utils.tidyDataDir(config['data_dir'], server.feedStore, btControl.rawServer)
	btControl.rawServer.install_sigint_handler()
	if useHelper:
		btControl.rawServer.add_task(MacLauncher.startHelper, 0, (config['control_port'], server.address[1], btControl))
	btControl.rawServer.add_task(display.info, 0, ('Ready on ' + prefix,))
	btControl.start()


if __name__ == '__main__':
	try:
		if 'RESOURCEPATH' in os.environ:
			MacLauncher.launch()
		else:
			main()
		# end if
	except SystemExit:
		pass
	except:
		utils.debugDump()
		raise
	# end try
